﻿// Project: Salient
// http://salient.codeplex.com
// 
// Copyright 2010, Sky Sanders <sky at skysanders.net>
// licensed under the GPL Version 2 licenses.
// http://salient.codeplex.com/license
// 
// 
// You may use this code under the terms of the GNU General Public 
// License (GPL) Version 2.
using System;
using System.IO;
using System.Net;
using System.Text;
using System.Threading;

namespace Salient.Net
{
    ///<summary>
    ///</summary>
    public class Gateway : IGateway
    {
        private readonly ManualResetEvent _throttleGate = new ManualResetEvent(false);
        private readonly object _throttleLock = new object();
        private IGatewayCache _cache;
        private DateTime _lastRequestTime = DateTime.MinValue;

        #region IGateway Members

        /// <summary>
        /// The minimum length of time to allow between requests.
        /// </summary>
        public int ThrottleInterval { get; set; }

        /// <summary>
        /// The user-agent header to apply to all requests.
        /// </summary>
        public string UserAgent { get; set; }

        /// <summary>
        /// The IGatewayCache that services this gateway.
        /// </summary>
        public IGatewayCache Cache
        {
            get { return _cache; }
            set { _cache = value; }
        }
#if !SILVERLIGHT
        /// <summary>
        /// The System.Net.IWebProxy instance assigned to this gateway.
        /// </summary>
        public IWebProxy Proxy { get; set; }

#endif
        /// <summary>
        ///   <para>This method first checks the cache for the specified url, if found the process results are returned immediately.</para>
        ///   <para>If a cached result is not found, the request is throttled as per the ThrottleInterval, and upon release, an AsyncResult is returned.</para>
        ///   <para>Upon successful completion of the request, the response text is passed to the requestHandler, which returns an instance of T.</para>
        ///   <para>This value is cached after the callback completes, allowing the callback to examine and modify the results before caching.</para>
        /// </summary>
        /// <typeparam name = "T">The type that is returned by the requestHandler</typeparam>
        /// <param name = "url">The path of the request.</param>
        /// <param name = "callback">The function that handles the completion of the request</param>
        /// <param name = "requestHandler">The function that processes the request and 
        ///   returns an instance of <typeparamref name = "T"></typeparamref></param>
        /// <returns></returns>
        public GatewayAsyncResult<T> BeginGetResponse<T>(string url, GatewayAsyncCallback<T> callback,
                                                         GatewayRequestHandlerDelegate<T> requestHandler)
            where T : class, new()
        {
            return BeginGetResponse(url, "/", false, 30000, callback, requestHandler, null);
        }

        /// <summary>
        ///   <para>This method first checks the cache for the specified url, if found the process results are returned immediately.</para>
        ///   <para>If a cached result is not found, the request is throttled as per the ThrottleInterval, and upon release, an AsyncResult is returned.</para>
        ///   <para>Upon successful completion of the request, the response text is passed to the requestHandler, which returns an instance of T.</para>
        ///   <para>This value is cached after the callback completes, allowing the callback to examine and modify the results before caching.</para>
        /// </summary>
        /// <typeparam name = "T">The type that is returned by the requestHandler</typeparam>
        /// <param name = "url">The path of the request.</param>
        /// <param name = "cacheRule">
        ///   <para>The rule to apply when getting/setting from the gateway cache. </para>
        ///   <para>e.g. for StackOverflow API, this would be the route. 
        ///     '/sites' should have a rule of 2 hours applied.</para>
        /// </param>
        /// <param name = "bypassCache">whether to bypass the gateway's cache</param>
        /// <param name = "timeout">when to force a timeout error, in ms.</param>
        /// <param name = "callback">The function that handles the completion of the request</param>
        /// <param name = "requestHandler">The function that processes the request and 
        ///   returns an instance of <typeparamref name = "T"></typeparamref></param>
        /// <returns></returns>
        public GatewayAsyncResult<T> BeginGetResponse<T>(string url, string cacheRule, bool bypassCache, int? timeout,
                                                         GatewayAsyncCallback<T> callback,
                                                         GatewayRequestHandlerDelegate<T> requestHandler)
            where T : class, new()
        {
            return BeginGetResponse(url, cacheRule, bypassCache, timeout, callback, requestHandler, null);
        }

        /// <summary>
        ///   <para>This method first checks the cache for the specified url, if found the process results are returned immediately.</para>
        ///   <para>If a cached result is not found, the request is throttled as per the ThrottleInterval, and upon release, an AsyncResult is returned.</para>
        ///   <para>Upon successful completion of the request, the response text is passed to the requestHandler, which returns an instance of T.</para>
        ///   <para>This value is cached after the callback completes, allowing the callback to examine and modify the results before caching.</para>
        /// </summary>
        /// <typeparam name = "T">The type that is returned by the requestHandler</typeparam>
        /// <param name = "url">The path of the request.</param>
        /// <param name = "cacheRule">
        ///   <para>The rule to apply when getting/setting from the gateway cache. </para>
        ///   <para>e.g. for StackOverflow API, this would be the route. 
        ///     '/sites' should have a rule of 2 hours applied.</para>
        /// </param>
        /// <param name = "bypassCache">whether to bypass the gateway's cache</param>
        /// <param name = "timeout">when to force a timeout error, in ms.</param>
        /// <param name = "callback">The function that handles the completion of the request</param>
        /// <param name = "requestHandler">The function that processes the request and 
        ///   returns an instance of <typeparamref name = "T"></typeparamref></param>
        /// <param name = "state">State object to be used for request. Useful for tagging a request with an id via RequestState.Data</param>
        /// <returns></returns>
        public GatewayAsyncResult<T> BeginGetResponse<T>(string url, string cacheRule, bool bypassCache, int? timeout,
                                                         GatewayAsyncCallback<T> callback,
                                                         GatewayRequestHandlerDelegate<T> requestHandler,
                                                         GatewayRequestState state) where T : class, new()
        {
            state = state ?? new GatewayRequestState();
            state.CacheRule = cacheRule;
            state.Url = url;
            state.Timeout = timeout;


            // this will contain metrics for the request upon return
            var result = new GatewayAsyncResult<T>(callback, state);


            if (!bypassCache)
            {
                object cached;

                lock (_cache)
                {
                    cached = Cache.Get(url);
                }

                if (cached != null)
                {
                    state.Cached = true;
                    result.Complete((T) cached, true);
                    return result; // <--- take the cache and exit early, no throttle charge
                }
            }


            lock (_throttleLock)
            {
                // if the last request was within the throttle interval, lets
                // just chill a bit and wait.
                if (_lastRequestTime.AddMilliseconds(ThrottleInterval) > DateTime.Now)
                {
                    var waitTime =
                        (int) (_lastRequestTime.AddMilliseconds(ThrottleInterval) - DateTime.Now).TotalMilliseconds;
                    _throttleGate.WaitOne(waitTime);
                    _throttleGate.Reset();
                }

                // make a not of the time so we can check the next request to come through this lock
                double requestInterval = (DateTime.Now - _lastRequestTime).TotalMilliseconds;
                state.RequestInterval = requestInterval;
                _lastRequestTime = DateTime.Now;
            }


            HttpWebRequest request = GetWebRequest(url);
            state.Request = request;
            Exception getResponseException = null;
            T processed = null;

            var timeoutWait = new AutoResetEvent(false);

            AsyncCallback innerCallback = ar =>
                {
                    try
                    {
                        WebResponse response = request.EndGetResponse(ar);
                        state.Response = (HttpWebResponse) response;
                        if (requestHandler != null)
                        {
                            processed = requestHandler(state);
                        }
                    }
                    catch (Exception e)
                    {
                        getResponseException = e;
                    }
                    finally
                    {
                        timeoutWait.Set();
                    }
                };


            IAsyncResult asyncResult = request.BeginGetResponse(innerCallback, null);

            if (asyncResult.CompletedSynchronously)
            {
                result.Complete(processed, true);
                SetCacheItem(url, cacheRule, processed);
            }
            else
            {
                if (!timeoutWait.WaitOne(timeout ?? 30000))
                {
                    result.HandleException(new TimeoutException("No response received in time."), false);
                }
                else
                {
                    if (getResponseException != null)
                    {
                        result.HandleException(getResponseException, false);
                    }
                    else
                    {
                        result.Complete(processed, false);
                        SetCacheItem(url, cacheRule, processed);
                    }
                }
            }

            return result;
        }

     
        #endregion

        private void SetCacheItem(string uri, string ruleKey, object data)
        {
            Cache.Set(uri, ruleKey, data);
        }

        private HttpWebRequest GetWebRequest(string uri)
        {
            var webRequest = (HttpWebRequest) WebRequest.Create(uri);

#if !SILVERLIGHT
            webRequest.Method = "GET";
            webRequest.UserAgent = UserAgent;
            webRequest.AutomaticDecompression = DecompressionMethods.GZip;
            webRequest.Headers.Add(HttpRequestHeader.AcceptEncoding, "gzip");

            // not sure this is working the way i think it is.
            if (Proxy != null)
            {
                webRequest.Proxy = Proxy;
            }
#endif

            return webRequest;
        }
    }
}
